/*******************************************************************************
 * PushButton Engine
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the License.html file at the root directory of this SDK.
 ******************************************************************************/
package com.org.system.profiler
{
	import com.org.system.logging.Log;
	import com.org.system.utils.NumberUtil;
	import com.org.system.utils.StringUtil;
	
	import flash.utils.getTimer;

	/**
		* Simple, static hierarchical block profiler.
		*
		* Currently it is hardwired to start measuring when you press P, and dump
		* results to the log when you let go of P. Eventually something more
		* intelligent will be appropriate.
		*
		* Use it by calling Profiler.enter("CodeSectionName"); before the code you
		* wish to measure, and Profiler.exit("CodeSectionName"); afterwards. Note
		* that Enter/Exit calls must be matched - and if there are any branches, like
		* an early return; statement, you will need to add a call to Profiler.exit()
		* before the return.
		*
		* Min/Max/Average times are reported in milliseconds, while total and non-sub
		* times (times including children and excluding children respectively) are
		* reported in percentages of total observed time.
		*/
	public class Profiler
	{
		public var enabled:Boolean = true;
		public var nameFieldWidth:int = 50;
		public var indentAmount:int = 3;
		
		/**
		 * Because we have to keep the stack balanced, we can only enabled/disable
		 * when we return to the root node. So we keep an internal flag.
		 */
		private var _wantReport:Boolean = true;
		private var _wantWipe:Boolean = false;
		private var _stackDepth:int = 0;
		private var _rootNode:ProfileInfo;
		private var _currentNode:ProfileInfo;

		private static var _instance:Profiler;
		
		public static function getInstance() : Profiler
		{
			if (_instance == null) {
				_instance = new Profiler;
			}
			return _instance;
		}
		
		public static function enter(blockName:String) : void
		{
			getInstance().enter(blockName);
		}
		
		public static function exit(blockName:String) : void
		{
			getInstance().exit(blockName);
		}
				
		public static function report() : void
		{
			getInstance().report();
		}
		
		public static function wipe() : void
		{
			getInstance().wipe();
		}
		
		public static function ensureAtRoot() : void
		{
			getInstance().ensureAtRoot();
		}
		
		public function init(initParams:*=null) : void
		{
			if (initParams != null) {
				for (var i:String in initParams) {
					this[i] = initParams[i];
				}
			}
		}
		
		/**
		 * Indicate we are entering a named execution block.
		 */
		public function enter(blockName:String):void
		{
			if(!enabled)
				return;
			
			if(!_currentNode)
			{
				_rootNode = new ProfileInfo("Root")
				_currentNode = _rootNode;
			}
			 
			if(_wantWipe)
				doWipe();
			
			if(_wantReport)
				doReport();
			 
			// Update stack depth and early out.
			_stackDepth++;
					
			 // Look for child; create if absent.
			var newNode:ProfileInfo = _currentNode.children[blockName];
			if(!newNode)
			{
				newNode = new ProfileInfo(blockName, _currentNode);
				_currentNode.children[blockName] = newNode;
			}
			 
			// Push onto stack.
			_currentNode = newNode;
			 
			// Start timing the child node. Too bad you can't QPC from Flash. ;)
			_currentNode.startTime = flash.utils.getTimer();
		}
		
		/**
		 * Indicate we are exiting a named exection block.
		 */
		public function exit(blockName:String):void
		{
			if(!enabled)
				return;

			// Update stack depth and early out.
			_stackDepth--;
			 
			if(blockName != _currentNode.name)
				throw new Error("Mismatched Profiler.enter/Profiler.exit calls, got '" + _currentNode.name + "' but was expecting '" + blockName + "'");
			 
			// Update stats for this node.
			var elapsedTime:int = flash.utils.getTimer() - _currentNode.startTime;
			_currentNode.activations++;
			_currentNode.totalTime += elapsedTime;
			if(elapsedTime > _currentNode.maxTime) _currentNode.maxTime = elapsedTime;
			if(elapsedTime < _currentNode.minTime) _currentNode.minTime = elapsedTime;

			// Pop the stack.
			_currentNode = _currentNode.parent;
		}
		
		/**
		 * Dumps statistics to the log next time we reach bottom of stack.
		 */
		public function report():void
		{
			if(_stackDepth)
			{
				_wantReport = true;
				return;
			}
			 
			doReport();
		}
		
		/**
		 * Reset all statistics to zero.
		 */
		public function wipe():void
		{
			if(_stackDepth)
			{
				_wantWipe = true;
				return;
			}
			 
			doWipe();
		}
		
		/**
		 * Call this outside of all Enter/Exit calls to make sure that things
		 * have not gotten unbalanced. If all enter'ed blocks haven't been
		 * exit'ed when this function has been called, it will give an error.
		 *
		 * Useful for ensuring that profiler statements aren't mismatched.
		 */
		public function ensureAtRoot():void
		{
			if(_stackDepth)
				throw new Error("Not at root!");
		}
		
		private function doReport():void
		{
			_wantReport = false;
			 
			var header:String = StringUtil.sprintf( "%-" + nameFieldWidth + "s%-8s%-8s%-8s%-8s%-8s%-8s", "name", "Calls", "Total%", "NonSub%", "AvgMs", "MinMs", "MaxMs" );
			Log.profiler(header);
			report_R(_rootNode, 0);
		}
		
		private function report_R(pi:ProfileInfo, indent:int):void
		{
			 // Figure our display values.
			var selfTime:Number = pi.totalTime;

			var hasKids:Boolean = false;
			var totalTime:Number = 0;
			for each(var childPi:ProfileInfo in pi.children)
			{
				hasKids = true;
				selfTime -= childPi.totalTime;
				totalTime += childPi.totalTime;
			}
			 
			 // Fake it if we're root.
			if(pi.name == "Root")
				pi.totalTime = totalTime;
			 
			var displayTime:Number = -1;
			if(pi.parent)
				displayTime = Number(pi.totalTime) / Number(_rootNode.totalTime) * 100;
					
			var displayNonSubTime:Number = -1;
			if(pi.parent)
				displayNonSubTime = selfTime / Number(_rootNode.totalTime) * 100;
			 
			// Print us.
			var entry:String = null;
			if(indent == 0)
			{
				entry = "+Root";
			}
			else
			{
				entry = StringUtil.sprintf( "%-" + (indent * indentAmount) + "s%-" + (nameFieldWidth - indent * indentAmount) + "s%-8s%-8s%-8s%-8s%-8s%-8s", "",
						(hasKids ? "+" : "-") + pi.name, pi.activations, displayTime.toFixed(2), displayNonSubTime.toFixed(2), (Number(pi.totalTime) / Number(pi.activations)).toFixed(1), pi.minTime, pi.maxTime);						 
			}
			Log.profiler(entry);
			 
			 // Sort and draw our kids.
			var tmpArray:Array = new Array();
			for each(childPi in pi.children)
				tmpArray.push(childPi);
			tmpArray.sortOn("totalTime", Array.NUMERIC | Array.DESCENDING);
			for each(childPi in tmpArray)
				report_R(childPi, indent + 1);
		}

		private function doWipe(pi:ProfileInfo = null):void
		{
			_wantWipe = false;
			 
			if(!pi)
			{
				doWipe(_rootNode);
				return;
			}
			 
			pi.wipe();
			for each(var childPi:ProfileInfo in pi.children)
				doWipe(childPi);
		}			
	}
}

final class ProfileInfo
{
	public var name:String;
	public var children:Object = {};
	public var parent:ProfileInfo;
	 
	public var startTime:int, totalTime:int, activations:int;
	public var maxTime:int = int.MIN_VALUE;
	public var minTime:int = int.MAX_VALUE;
	 
	final public function ProfileInfo(n:String, p:ProfileInfo = null)
	{
		name = n;
		parent = p;
	}
	 
	final public function wipe():void
	{
		startTime = totalTime = activations = 0;
		maxTime = int.MIN_VALUE;
		minTime = int.MAX_VALUE;
	}
}